package fsm

import "sync"

type (
	ActionHandler func(from, to, event string, ctx *Context) error
)

type (
	Delegate interface {
		AfterTransitionCausedError(from, to, event string, ctx *Context)
		BeforeTransitionBegin(from, to, event string, ctx *Context)
		AfterTransitionCompleted(from, to, event string, ctx *Context)
		AfterTransitionEnd(from, to, event string, ctx *Context)
		AfterTransitionDeclined(from, to, event string, ctx *Context)
		BeforeActionInvoked(from, to, event string, ctx *Context)
		AfterActionInvoked(from, to, event string, ctx *Context)
		HandleEvent(action ActionHandler, from, to, event string, ctx *Context) error
	}

	DefaultDelegate struct {
	}
)

func (d *DefaultDelegate) AfterTransitionCausedError(from, to, event string, ctx *Context) {
}

func (d *DefaultDelegate) BeforeTransitionBegin(from, to, event string, ctx *Context) {
}

func (d *DefaultDelegate) AfterTransitionCompleted(from, to, event string, ctx *Context) {
}

func (d *DefaultDelegate) AfterTransitionEnd(from, to, event string, ctx *Context) {
}

func (d *DefaultDelegate) AfterTransitionDeclined(from, to, event string, ctx *Context) {
}

func (d *DefaultDelegate) BeforeActionInvoked(from, to, event string, ctx *Context) {
}

func (d *DefaultDelegate) AfterActionInvoked(from, to, event string, ctx *Context) {
}

func (d *DefaultDelegate) HandleEvent(action ActionHandler, from, to, event string, ctx *Context) error {
	defer d.AfterTransitionEnd(from, to, event, ctx)
	if from != to {
		d.BeforeTransitionBegin(from, to, event, ctx)
	}

	d.BeforeActionInvoked(from, to, event, ctx)
	err := action(from, to, event, ctx)
	d.AfterActionInvoked(from, to, event, ctx)
	if err != nil {
		d.AfterTransitionCausedError(from, to, event, ctx)
		return err
	} else {
		d.AfterTransitionCompleted(from, to, event, ctx)
		return nil
	}
}

type (
	StateMachineDataStore interface {
		SetError(err error)
		SetNewState(initial bool, new string)
		CurrentState() interface{}
		LastState() interface{}
		InitialState() interface{}
		LastError() error
	}

	StringerStateMachineDataStore struct {
		mu           *sync.RWMutex
		lastState    string
		initialState string
		currentState string
		lastError    error
	}
)

func (store *StringerStateMachineDataStore) SetError(err error) {
	store.mu.Lock()
	defer store.mu.Unlock()
	store.lastError = err
}

func (store *StringerStateMachineDataStore) SetNewState(initial bool, new string) {
	store.mu.Lock()
	defer store.mu.Unlock()
	if initial {
		store.initialState = new
	} else {
		store.lastState = store.currentState
		store.currentState = new
	}
}

func (store *StringerStateMachineDataStore) CurrentState() interface{} {
	store.mu.RLock()
	defer store.mu.RUnlock()
	return store.currentState
}

func (store *StringerStateMachineDataStore) LastState() interface{} {
	store.mu.RLock()
	defer store.mu.RUnlock()
	return store.lastState
}

func (store *StringerStateMachineDataStore) InitialState() interface{} {
	store.mu.RLock()
	defer store.mu.RUnlock()
	return store.initialState
}

func (store *StringerStateMachineDataStore) LastError() error {
	store.mu.RLock()
	defer store.mu.RUnlock()
	return store.lastError
}

type (
	Transition struct {
		From   string
		To     string
		Event  string
		Action ActionHandler
	}

	StateMachine struct {
		data        StateMachineDataStore
		delegate    Delegate
		transitions []*Transition
	}
)

func New(delegate Delegate, transitions ...*Transition) *StateMachine {
	defaultDataStore := &StringerStateMachineDataStore{
		mu:           new(sync.RWMutex),
		lastState:    "",
		initialState: "",
		currentState: "",
		lastError:    nil,
	}
	return &StateMachine{
		data:        defaultDataStore,
		delegate:    delegate,
		transitions: transitions,
	}
}

func NewDefaultDelegate() *DefaultDelegate {
	return &DefaultDelegate{}
}

func (sm *StateMachine) Fire(current, event string, initial bool, ctx *Context) error {
	var transition *Transition
	// find matching transition
FindMatchTrans:
	for _, trans := range sm.transitions {
		if trans.From == current && trans.Event == event {
			transition = trans
			break FindMatchTrans
		}
	}

	if transition == nil {
		return StateMachineError{
			event: event,
			state: current,
		}
	}

	if initial {
		sm.data.SetNewState(initial, current)
	}

	sm.data.SetNewState(false, transition.To)

	var err error
	if transition.Action != nil {
		err = sm.delegate.HandleEvent(transition.Action, current, transition.To, event, ctx)
	}
	if err != nil {
		sm.data.SetError(err)
	}

	return err
}

func (sm *StateMachine) GetCurrentState() string {
	return sm.data.CurrentState().(string)
}

func (sm *StateMachine) GetLastError() error {
	return sm.data.LastError()
}

type (
	Context struct {
		data map[string]interface{}
	}
)

func NewContext() *Context {
	return &Context{data: make(map[string]interface{})}
}

func (c *Context) Set(key string, val interface{}) interface{} {
	if origin, ok := c.data[key]; ok {
		c.data[key] = val
		return origin
	} else {
		c.data[key] = val
		return nil
	}
}

func (c *Context) Get(key string) interface{} {
	val, ok := c.data[key]
	if !ok {
		return nil
	}
	return val
}
